
#ifndef AFTERMATH_ALGEBRA_MATRIX_HPP_INCLUDED
#define AFTERMATH_ALGEBRA_MATRIX_HPP_INCLUDED

#include <cstdint>
#include <cstring> // for std::memcpy and std::memset
#include <new>
#include <stdexcept>
#include <utility>
#include <vector>

namespace aftermath
{
    namespace algebra
    {
        namespace detail
        {
            template <bool t_is_row_major = true>
            struct matrix_arrangement
            {
                static std::size_t flatten(std::size_t row, std::size_t column, std::size_t height, std::size_t)
                {
                    return column * height + row;
                }
            };

            template <>
            struct matrix_arrangement<false>
            {
                static std::size_t flatten(std::size_t row, std::size_t column, std::size_t, std::size_t width)
                {
                    return row * width + column;
                }
            };
        }

        template <typename t_data_type, bool t_is_row_major = true>
        struct matrix;

        template <typename t_data_type>
        using matrix_row_major = matrix<t_data_type, true>;

        template <typename t_data_type>
        using matrix_column_major = matrix<t_data_type, false>;

        template <typename t_data_type, bool t_is_row_major>
        struct matrix
        {
            static const bool is_row_major = t_is_row_major;
            static const bool is_column_major = !t_is_row_major;

            typedef t_data_type data_type;
            typedef t_data_type value_type;
            typedef matrix<data_type, is_row_major> type;

        private:
            typedef detail::matrix_arrangement<is_row_major> arrangement_type;

            data_type* m_data_pointer = nullptr;
            std::size_t m_height;
            std::size_t m_width;
            std::size_t m_size;

            void copy_raw(std::size_t height, std::size_t width, const data_type* data_pointer)
            {
                if (this->m_data_pointer == nullptr) return;
                if (data_pointer == nullptr) throw new std::invalid_argument("<data_pointer> cannot be nullptr.");
                std::memcpy(this->m_data_pointer, data_pointer, (height * width) * sizeof(data_type));
            }

        public:
            matrix()
                : m_height(0UL), m_width(0UL), m_size(0UL)
            {

            }

            matrix(std::size_t height, std::size_t width)
                : m_height(height), m_width(width), m_size(height * width)
            {
                if (height == 0UL || width == 0UL) return;
                this->m_data_pointer = new data_type[this->m_size];
            }

            /** Creates a matrix with all entries set to <value>. */
            matrix(std::size_t height, std::size_t width, const data_type& value)
                : matrix(height, width)
            {
                this->fill(value);
            }

            /** Creates a matrix as a copy. */
            matrix(std::size_t height, std::size_t width, const data_type* data_pointer)
                : matrix(height, width)
            {
                this->copy_raw(height, width, data_pointer);
            }

            /** Creates a matrix as a copy. */
            matrix(const type& other)
                : matrix(other.m_height, other.m_width, other.m_data_pointer)
            {
            }

            /** Creates a matrix by stealing from <other>. */
            matrix(type&& other)
            {
                *this = std::move(other);
            }

            /** Copies a matrix. */
            matrix& operator =(const type& other)
            {
                if (this != &other)
                {
                    // Resize check.
                    if (this->m_size != other.m_size)
                    {
                        this->m_height = other.m_height;
                        this->m_width = other.m_width;
                        this->m_size = other.m_size;

                        delete this->m_data_pointer; // Free up existing memory.
                        this->m_data_pointer = nullptr;
                    }
                    this->copy_raw(this->m_height, this->m_width, other.m_data_pointer);
                }
                return *this;
            }

            /** Copies a matrix by stealing from an rvalue. */
            matrix& operator =(type&& other)
            {
                this->steal(other);
                return *this;
            }

            ~matrix()
            {
                delete this->m_data_pointer;
                this->m_data_pointer = nullptr;

                this->m_height = 0UL;
                this->m_width = 0UL;
                this->m_size = 0UL;
            }

            /** Copies a matrix by stealing from the <other>. */
            void steal(type& other)
            {
                if (this != &other)
                {
                    delete this->m_data_pointer; // Clean up.
                    this->m_data_pointer = other.m_data_pointer;
                    other.m_data_pointer = nullptr;

                    this->m_height = other.m_height;
                    this->m_width = other.m_width;
                    this->m_size = other.m_size;

                    other.m_height = 0UL;
                    other.m_width = 0UL;
                    other.m_size = 0UL;
                }
            }

            /** Fills matrix with zeros. */
            void erase()
            {
                if (this->m_data_pointer == nullptr) return;
                std::memset(this->m_data_pointer, 0, this->m_size * sizeof(data_type));
            }

            /** Fills matrix with <value>. */
            void fill(const data_type& value)
            {
                for (std::size_t k = 0UL; k < this->m_size; k++) this->m_data_pointer[k] = value;
            }

            std::size_t get_height() const
            {
                return this->m_height;
            }

            std::size_t get_width() const
            {
                return this->m_width;
            }

            data_type& operator ()(std::size_t row_index, std::size_t column_index)
            {
                return this->m_data_pointer[arrangement_type::flatten(row_index, column_index, this->m_height, this->m_width)];
            }

            const data_type& operator ()(std::size_t row_index, std::size_t column_index) const
            {
                return this->m_data_pointer[arrangement_type::flatten(row_index, column_index, this->m_height, this->m_width)];
            }
        };
    }
}

#endif // AFTERMATH_ALGEBRA_MATRIX_HPP_INCLUDED
